/*
    KMC Simulation
    Copyright (c) 2012-14 Filip Rozbořil

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "includes.h"
#include "GridObject.h"
#include "HopRate.h"
#include "KmcSettings.h"
#include "Random.h"
#include "ZobristHash.h"

typedef unordered_map<GridField, GridObject*>::value_type FieldObjectPair;
typedef unordered_map<GridField, GridObject*>::iterator FieldObjectIt;

class Grid
{
	private:
		static const double mBoltzmannConst;
		uint32_t mRows;
		uint32_t mCols;
		uint64_t mElements;
		uint32_t mObjectReservation, mVectorReservation;
		double mCoverage, mTemperature;
		vector<AlAtom> mAtoms;
		unordered_map<GridField, GridObject*> mObjects;
		vector<double> mEnergies;
		double mBarrierVertical, mBarrierHorizontal, mFrequencyVertical, mFrequencyHorizontal;
		vector<HopRate*> mHopRates;
		ZobristHash mStateHash; //this hash is related to the "state" of the grid as determined by the number of atoms in each HopRate
								//identical hashes mean identical rates of all events
								//can be used for caching the rates of determining when to recalculate the rates
		zhash_t mLastHash;
			
		uint32_t wrapRow(uint32_t y) { return (y + mRows) & (mRows - 1); }	//we can use bitwise AND because mRows, mCols are guaranteed to be a power of 2
		uint32_t wrapCol(uint32_t x) { return (x + mCols) & (mCols - 1); }	//bitwise AND is up to 10x faster than modulo operation
																			//Testing showed that changing % to & speeds up the simulation by 
																			//14% on Intel Q9550 (g++ 4.6.3) and by over 30% on Opteron 6128 (g++ 4.6.1)!
		void updateNeighbors(GridField& gf);
		void refreshAtomEvent(GridObject * go, Direction::type dir);
		void refreshAtomEvents(GridObject * go);
		void setAtomSite(GridAtom* atom, Direction::type dir);
		Site::type getSiteType(GridField& gf, GridObject* self);
		void refreshEvents(GridAtom* atom);
		void removeAllEvents(GridAtom* atom);
		void removeEvent(GridAtom* atom, Direction::type dir);
		void addAllEvents(GridAtom* atom);
		void addEvent(GridAtom* atom, Direction::type dir);
		
	public:
		Grid(KmcSettings& sett);
		double calculateFrequency(Site::type from, Site::type to, Direction::type dir);
		uint32_t getKinkCount();
		GridAtom* createAtom();
		void removeAtom(GridAtom* atom);
		void placeAtom(GridAtom* atom, GridField gf);
		
		GridObject* getObject(GridField gf) { auto it = mObjects.find(gf); return it != mObjects.end() ? it->second : &GridObject::None; }
		bool isOccupied(GridField gf) { return mObjects.find(gf) != mObjects.end(); }
		bool isBlocked(GridField gf) { return mObjects.find(getLeft(gf)) != mObjects.end() || mObjects.find(getRight(gf)) != mObjects.end(); }
		GridField getField(GridField gf, Direction::type dir) { return GridField(wrapCol(gf.getx() + Direction::projectx(dir)), wrapRow(gf.gety() + Direction::projecty(dir))); }
		GridField getTop(GridField gf) {	return GridField(gf.getx(), wrapRow(gf.gety()+1));	}
		GridField getBottom(GridField gf) {	return GridField(gf.getx(), wrapRow(gf.gety()-1));	}
		
		GridField getFarBottomRight(GridField gf) {	return GridField(wrapCol(gf.getx()+1), wrapRow(gf.gety()-2));	}
		GridField getBottomFarRight(GridField gf) {	return GridField(wrapCol(gf.getx()+2), wrapRow(gf.gety()-1));	}
		GridField getFarBottomLeft(GridField gf) {	return GridField(wrapCol(gf.getx()-1), wrapRow(gf.gety()-2));	}
		GridField getBottomFarLeft(GridField gf) {	return GridField(wrapCol(gf.getx()-2), wrapRow(gf.gety()-1));	}
		
		GridField getFarTopRight(GridField gf) {	return GridField(wrapCol(gf.getx()+1), wrapRow(gf.gety()+2));	}
		GridField getTopFarRight(GridField gf) {	return GridField(wrapCol(gf.getx()+2), wrapRow(gf.gety()+1));	}
		GridField getFarTopLeft(GridField gf) {		return GridField(wrapCol(gf.getx()-1), wrapRow(gf.gety()+2));	}
		GridField getTopFarLeft(GridField gf) {		return GridField(wrapCol(gf.getx()-2), wrapRow(gf.gety()+1));	}

		
		GridField getLeft(GridField gf) { return GridField(wrapCol(gf.getx()-1), gf.gety()); }
		GridField getRight(GridField gf) { return GridField(wrapCol(gf.getx()+1), gf.gety()); }
		GridField getFarLeft(GridField gf) { return GridField(wrapCol(gf.getx()-2), gf.gety()); }
		GridField getFarRight(GridField gf) { return GridField(wrapCol(gf.getx()+2), gf.gety()); }
		GridField getFarTop(GridField gf) { return GridField(gf.getx(), wrapRow(gf.gety()+2)); }
		GridField getFarBottom(GridField gf) { return GridField(gf.getx(), wrapRow(gf.gety()-2)); }
		GridField getTopLeft(GridField gf) { return GridField(wrapCol(gf.getx()-1), wrapRow(gf.gety()+1));	}
		GridField getTopRight(GridField gf) { return GridField(wrapCol(gf.getx()+1), wrapRow(gf.gety()+1)); }
		GridField getBottomLeft(GridField gf) { return GridField(wrapCol(gf.getx()-1), wrapRow(gf.gety()-1));	}
		GridField getBottomRight(GridField gf) { return GridField(wrapCol(gf.getx()+1), wrapRow(gf.gety()-1)); }
		uint32_t getRows() { return mRows; }
		uint32_t getCols() { return mCols; }
		uint64_t getElements() { return mElements; }
		uint32_t getAtomCount() { return mAtoms.size(); }
		double getCoverage() { return mCoverage; }
		HopRate* getRate(Site::type from, Site::type to, Direction::type dir) { return mHopRates[Direction::Count * Site::Count * from + Direction::Count * to + dir]; }
		void addHopRate(HopRate* hrt) { mHopRates.push_back(hrt); mStateHash.togglePosition(hrt->getHash()); }
		uint32_t getListCapacity() { return mVectorReservation; }
		zhash_t getHash() { return mStateHash.getValue(); }
		bool hasStateChanged() { if(mStateHash.getValue() != mLastHash) { mLastHash = mStateHash.getValue(); return true; } return false; }
		GridField getRandomPosition(Random* r);
		GridField getSafePosition(GridField gf, Random* r);
		GridField randomWalk(GridField gf, Random* r);
		int32_t distanceBetween(GridField gf1, GridField gf2);
		
#ifdef GRIDCHECK
		void checkConsistency();
		void checkNeighbors(GridAtom* atom);
#endif
		
		
	friend ostream& operator<<(ostream& os, Grid& g);
};

inline GridField Grid :: randomWalk(GridField gf, Random* r)
{
 	if(r->getBoolean())
	{
		if(r->getBoolean())
		{
			return getLeft(gf);
		}
		else
		{
			return getRight(gf);
		}
	}
	else
	{
	 	if(r->getBoolean())
		{
			return getTop(gf);
		}
		else
		{
			return getBottom(gf);
		}
	}
}

inline GridField Grid :: getSafePosition(GridField target, Random* r)
{
	while(isOccupied(target) || isBlocked(target))
 	{
 		target = randomWalk(target, r);
 	}
 	
 	return target;
}

inline GridField Grid :: getRandomPosition(Random* r)
{
 	GridField target(
		r->getInteger(getRows()),
 		r->getInteger(getCols())
 	);
 	
 	return getSafePosition(target, r);
}

inline int32_t Grid :: distanceBetween(GridField gf1, GridField gf2)
{
 	uint32_t dx = abs(gf1.getx() - gf2.getx());
 	if(dx > mCols / 2) dx = mCols - dx;
 	uint32_t dy = abs(gf1.gety() - gf2.gety());
 	if(dy > mRows / 2) dy = mRows - dy;
 	
 	//Manhattan distance
 	return dx + dy;
}

